package windowlimiter

import (
	"context"
	"fmt"
	"sync"
	"sync/atomic"
	"time"

	"gitee.com/mqyqingkong/limiter"
)

type fixWindowLimiter struct {
	limit           int64
	window          time.Duration
	acquireNum      int64
	resetWindowTime time.Time
	lock            sync.Mutex
}

// NewFixWindowLimiter 基于固定窗口（window）限流。window时间窗口内，最多获取limit个许可。
func NewFixWindowLimiter(window time.Duration, limit int64) limiter.Limiter {
	windowLimiter := &fixWindowLimiter{
		window:          window,
		limit:           limit,
		resetWindowTime: time.Now(),
	}
	return windowLimiter
}

// Acquire 请求许可，返回值为true时，表示已获得许可，为false时，表示未获得许可。
func (w *fixWindowLimiter) Acquire() bool {
	w.tryResetIfPossible()
	acqNum := atomic.AddInt64(&w.acquireNum, 1)
	if acqNum > w.limit {
		return false
	} else {
		return true
	}
}

func (w *fixWindowLimiter) String() string {
	return fmt.Sprintf("rolling window duration:%d s, acquire limit:%d", w.window, w.limit)
}

func (w *fixWindowLimiter) tryResetIfPossible() {
	w.lock.Lock()
	defer w.lock.Unlock()
	if time.Since(w.resetWindowTime) >= w.window {
		w.resetWindowTime = time.Now()
		w.acquireNum = 0
	}
}

const InfDuration = time.Duration(1<<63 - 1)

// TryAcquire 尝试获取许可，返回值为nil时，表示获得许可，否则未获得许可。
func (w *fixWindowLimiter) TryAcquire(ctx context.Context) error {
	acquired := w.Acquire()
	if acquired {
		return nil
	}

	select {
	case <-ctx.Done():
		return ctx.Err()
	default:
	}

	// Determine wait limit
	now := time.Now()
	waitLimit := InfDuration
	if deadline, ok := ctx.Deadline(); ok {
		waitLimit = deadline.Sub(now)

	}
	t := time.NewTimer(waitLimit)
	defer t.Stop()

	leftWindowLimitTimer := time.NewTimer(0)
	defer leftWindowLimitTimer.Stop()

	for {
		acquired := w.Acquire()
		if acquired {
			return nil
		} else {
			if !leftWindowLimitTimer.Stop() {
				<-leftWindowLimitTimer.C
			}
			leftWindowLimitTimer.Reset(w.getLeftWindowLimit())
		}
		select {
		case <-t.C:
			return nil
		case <-ctx.Done():
			return ctx.Err()
		case <-leftWindowLimitTimer.C:
		}
	}
}

func (w *fixWindowLimiter) getLeftWindowLimit() time.Duration {
	var leftWindowLimit time.Duration = 0
	resetWindowTime := w.resetWindowTime
	now := time.Now()
	usedWindowLimit := now.Sub(resetWindowTime)
	if usedWindowLimit < w.window {
		leftWindowLimit = w.window - usedWindowLimit

	}
	if leftWindowLimit < 0 {
		leftWindowLimit = 0
	}
	return leftWindowLimit
}
